package sg.edu.nus.comp.cs4218.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import sg.edu.nus.comp.cs4218.impl.ATool;
import sg.edu.nus.comp.cs4218.extended2.IUniqTool;

import java.util.*;

public class UniqTool extends ATool implements IUniqTool {
	public String currentLine = "";
	private boolean hasI;
	private boolean hasField;
	private boolean isStdin;
	private int num;
	private String fileName;
	
	/**
	 * Constructor
	 * 
	 * @param args
	 *            - Arguments the tool is going to be executed with.
	 */
	public UniqTool(String[] args) {
		super(args);
	}

	/**
	 * Compares currentLine and the input
	 * 
	 * @param checkCase 
	 *            - whether to ignore or check case when comparing uniqueness
	 * @param input
	 *            - the next line of currentLine
	 * @return returns currentLine if both are the same else both lines are returned.
	 */
	@Override
	public String getUnique(boolean checkCase, String input) {
		if (checkCase) {
			if (currentLine.equals(input)) {
				return currentLine;
			} else {
				return currentLine + "\n" + input;
			}
		} else {
			if (currentLine.equalsIgnoreCase(input)) {
				return currentLine;
			} else {
				return currentLine + "\n" + input;
			}
		}
	}

	/**
	 * Compares currentLine and the input after skipping num number of fields. 
	 * 
	 * @param num 
	 *            - number of fields to be skipped before checking for uniqueness
	 * @param input
	 *            - the next line of currentLine
	 *            
	 * @return If both lines are unique, return both lines. 
	 * If both lines are the same, return currentLine.
	 * if num <= 0, no fields will be skipped before checking for uniqueness
	 */
	@Override
	public String getUniqueSkipNum(int num, boolean checkCase, String input) {
		if (num <= 0) {
			return getUnique(checkCase, input);
		} else {
			StringTokenizer stCurrentLine = new StringTokenizer(currentLine);
			StringTokenizer stInput = new StringTokenizer(input);
			int index = 1;
			// when length of line < num of fields
			if (stCurrentLine.countTokens() <= num) {
					return currentLine + "\n" + input;
			} else {
				if (stInput.countTokens() <= num) {
					return currentLine + "\n" + input;
				} else if (stInput.countTokens() == stCurrentLine.countTokens()) {

					while (stCurrentLine.hasMoreTokens()) {
						String currentToken = stCurrentLine.nextToken();
						String inputToken = "";
						if (stInput.hasMoreTokens()) {
							inputToken = stInput.nextToken();
						}

						if (index > num) {
							if (checkCase) {
								if (!currentToken.equals(inputToken)) 
									return currentLine + "\n" + input;						
							} else {
								if (!currentToken.equalsIgnoreCase(inputToken)) 
									return currentLine + "\n" + input;
							}
						} else {
							index++;
						}
					}
					// both are the same after skipping some num of fields
					return currentLine;
				} else {
					return currentLine + "\n" + input;
				}
			}
		}
	}

	/**
	 * Display help for uniq command.
	 * 
	 * @return Options available for uniq command
	 */
	@Override
	public String getHelp() {
		return "uniq : Writes the unique lines in the given input. The input need not be sorted, but repeated input lines are detected only if they are adjacent."
				+ "\nCommand Format - uniq [OPTIONS] [FILE]"
				+ "\nFILE - Name of the file, when no file is present (denoted by '-') use standard input"
				+ "\nOPTIONS"
				+ "\n-f NUM : Skips NUM fields on each line before checking for uniqueness. Use a null"
				+ "\nstring for comparison if a line has fewer than n fields. Fields are sequences of"
				+ "\nnon-space non-tab characters that are separated from each other by at least one space or tab."
				+ "\nNUM is a single digit positive integer"
				+ "\n-i : Ignore differences in case when comparing lines."
				+ "\n-help : Brief information about supported options";
	}

	/**
	 * First, checks whether commands, options and fileName in args, stdin is valid.
	 * Second, call appropriate uniq methods according to options.
	 * 
	 * @param workingDir
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 *            
	 * @return Output generated by executing uniq command
	 */
	@Override
	public String execute(File workingDir, String stdin) {
		String result = "";
		currentLine = "";
		hasI = false;
		hasField = false;
		isStdin = false;
		num = -1;
		fileName = "";
		
		if (args != null) {
			// one pass to find -help
			for (int i = 0; i < args.length; i++) {
				if (args[i].equals("-help")) {
					setStatusCode(0);
					return getHelp();
				}
			}
			
			// set values needed to execute command if args is valid
			if(!checkArgsValidity(workingDir, stdin)) {
				setStatusCode(1);
				return "Invalid command";
			}

			// execute command
			if(isStdin){
				result = executeCommandForStdin(stdin);
			} else {
				BufferedReader br = null;
				// open file
				try {
					br = new BufferedReader(new FileReader(workingDir + File.separator						
							+ fileName));
	
					result = executeCommandForFile(br);
	
				} catch (IOException e) {
					e.printStackTrace();
					setStatusCode(1);
					return "Exception throw when reading file";
				} finally {
					try {
						if (br != null)
							br.close();
					} catch (IOException ex) {
						ex.printStackTrace();
					}
				}			
			}
			setStatusCode(0);
			return result;
		}
		setStatusCode(1);
		return "Invalid command";
	}
	
	// helper methods
	
	/**
	 * Read from file line by line and call appropriate uniq methods
	 * 
	 * @param br
	 *            - for the file
	 *            
	 * @return Output generated by executing uniq command
	 */
	private String executeCommandForFile (BufferedReader br)
			throws IOException {
		String result = "";
		String line;
		boolean isFirstLine = true;
		while ((line = br.readLine()) != null) {
			if (isFirstLine) {
				currentLine = line;
				isFirstLine = false;
			} else {
				String temp;
				if (hasField) 
					temp = getUniqueSkipNum(num, !hasI, line);
				else 
					temp = getUnique(!hasI, line);
				
				// not unique
				if (temp.equals(currentLine)) {
					if (result.length() == 0) 
						result = currentLine;
				}
				// unique
				else {
					if (result.length() == 0) 
						result = temp;
					 else 
						result += "\n" + line;
					currentLine = line;
				}
			}
		}
		return result;
	}

	/**
	 * Read from stdin by line and call appropriate uniq methods
	 * 
	 * @param stdin
	 *            - user input stdin
	 *            
	 * @return Output generated by executing uniq command
	 */
	private String executeCommandForStdin(String stdin) {
		String result = "";
		StringTokenizer st = new StringTokenizer(stdin, "\n");
		boolean isFirstLine = true;
		while(st.hasMoreTokens()) {
			String line = st.nextToken();
			if (isFirstLine) {
				currentLine = line;
				isFirstLine = false;
			} else {
				String temp;
				if (!hasField) 
					temp = getUniqueSkipNum(num, !hasI, line);
				else 
					temp = getUnique(!hasI, line);
				
				// not unique
				if (temp.equals(currentLine)) {
					if (result.length() == 0) 
						result = currentLine;
				}
				// unique
				else {
					if (result.length() == 0) 
						result = temp;
				    else 
						result += "\n" + line;
					currentLine = line;
				}
			}
		}
		return result;
	}
	
	/**
	 * Check args validity (command options, fileName, stdin) 
	 * and sets appropriate global variable for this object
	 * 
	 * @param workingDir 
	 *            - current working directory
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 *            
	 * @return false if workingDir/command/fileName/stdin is invalid, else true
	 */
	private boolean checkArgsValidity(File workingDir, String stdin) {
		// check whether command is valid
		try {
			if (args.length == 1) {
				// must be fileName
				fileName = args[0];
				if(!checkFileNameAndStdinValidity(workingDir, fileName, stdin))
					return false;
			} else if (args.length == 2) {
				// must be -i fileName
				if (args[0].equals("-i")) {
					fileName = args[1];
					if(!checkFileNameAndStdinValidity(workingDir, fileName, stdin))
						return false;
					hasI = true;
				} else 
					return false;
			} else if (args.length == 3) {
				// must be -f num fileName
				if (args[0].equals("-f")) {
					num = Integer.parseInt(args[1]);
					fileName = args[2];
					if(!checkFileNameAndStdinValidity(workingDir,fileName, stdin))
						return false;
					hasField = true;
				} else 
					return false;
			} else if (args.length == 4) {
				// must be -i -f num fileName
				if (args[0].equals("-i")) {
					if (args[1].equals("-f")) {
						num = Integer.parseInt(args[2]);
						fileName = args[3];
						if(!checkFileNameAndStdinValidity(workingDir, fileName, stdin))
							return false;			
						hasField = true;
						hasI = true;
					} else 
						return false;	
				} else if (args[0].equals("-f")) { // OR -f num -i fileName
					num = Integer.parseInt(args[1]);
					if (args[2].equals("-i")) {
						fileName = args[3];
						if(!checkFileNameAndStdinValidity(workingDir, fileName, stdin))
							return false;
						hasField = true;
						hasI = true;
					} else 
						return false;				
				} else 
					return false;			
			} else 
				return false;
		}
		catch (NumberFormatException e) {
			return false;
		}
		return true;
	}
	
	/**
	 * If fileName is - it checks that stdin is not null. 
	 * If fileName is not -, it checks that file exists and is not a directory
	 * 
	 * @param workingDir 
	 *            - current working directory
	 * @param fileName
	 *            - file name
	 * @param stdin
	 *            - user input content (to be used when file is not given)
	 *            
	 * @return returns false if the 2 above criteria isn't met
	 */
	private boolean checkFileNameAndStdinValidity(File workingDir, String fileName, String stdin){
		if(fileName.equals("-")) {
			if(stdin == null) {
				return false;
			}
			isStdin = true;
		} else {
			File f = new File(workingDir + "\\" + fileName);
			if (!f.exists() || f.isDirectory()) {
				return false;
			}
		}
		return true;
	}
	
}
